package csx.study.demo.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import csx.study.demo.dto.Result;
import csx.study.demo.entity.Shop;
import csx.study.demo.enums.CacheTTL;
import csx.study.demo.mapper.ShopMapper;
import csx.study.demo.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author csx
 * @since 2025-03-03
 */
//亮点：
//缓存穿透：缓存空值/布隆过滤器
//为什么是空值而不是一个特殊标记？因为是约定，或者说最佳实践
//一致性解决方案：cache aside策略，先读数据库，再删缓存，然后加事务
//缓存击穿（热点key突然失效）：随机过期时间，互斥锁/逻辑过期（永不过期），预热热点key
@Primary
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private static final String CACHE_SHOP_PREFIX="cache:shop:";
    private static final String LOCK_KEY_PREFIX="lock:shop:";


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //如果数据变更较少就用string类型，数据变更多再考虑hash那些结构
    // TODO用布隆过滤器解决缓存穿透问题
    @Override
    public Result queryShopById(Long id) {
        return queryShopByIdMutex(id);
    }

    //互斥锁
    public Result queryShopByIdMutex(Long id) {
        String key = CACHE_SHOP_PREFIX + id;

        // 1. 从Redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        // 2. 缓存命中（有效数据）
        if (StrUtil.isNotBlank(json)) {
            Shop shop = JSONUtil.toBean(json, Shop.class);
            return Result.ok(shop);
        }

        // 3. 缓存命中（空值防穿透）
        if (json != null) {  // 明确缓存了空值（""）
            return Result.fail("店铺不存在");
        }

        // 4. 实现缓存重建（互斥锁解决击穿）
        return getShopWithMutex(key, id);
    }

    private Result getShopWithMutex(String key, Long id) {
        String lockKey = LOCK_KEY_PREFIX + id;
        int retryCount = 0;
        final int MAX_RETRY = 5; // 最大重试次数
        final long SLEEP_TIME = 50; // 重试间隔(ms)

        while (retryCount < MAX_RETRY) {
            // 尝试获取分布式锁
            boolean locked = tryLock(lockKey);

            if (locked) {
                try {
                    // 获取锁成功 -> 二次检查缓存
                    String json = stringRedisTemplate.opsForValue().get(key);
                    if (StrUtil.isNotBlank(json)) {
                        return Result.ok(JSONUtil.toBean(json, Shop.class));
                    }
                    if (json != null) {
                        return Result.fail("店铺不存在");
                    }

                    // 查询数据库
                    Shop shop = getById(id);

                    // 处理数据库结果
                    if (shop == null) {
                        // 缓存空值（防穿透）
                        stringRedisTemplate.opsForValue().set(key, "", CacheTTL.NULL.getValue(), CacheTTL.NULL.getUnit());
                        return Result.fail("店铺不存在");
                    }

                    // 写入缓存
                    String shopJson = JSONUtil.toJsonStr(shop);
                    stringRedisTemplate.opsForValue().set(key, shopJson, CacheTTL.SHOP.getValue(), CacheTTL.SHOP.getUnit());
                    return Result.ok(shop);

                    //finally中的代码一定会执行，解决必须放在unlock中
                } finally {
                    unLock(lockKey);
                }
            }

            // 获取锁失败 -> 等待后重试
            retryCount++;
            try {
                //TODO 睡眠时间延长策略
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }

        log.warn("查询店铺超时");
        return Result.fail("查询超时");
    }


    private boolean tryLock(String key) {
        final long LOCK_SHOP_TTL = 10L;
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    //TODO 逻辑过期
    public Result queryShopByIdLogicExpire(Long id) {
        return Result.ok(getById(id));
    }

    //TODO 用布隆过滤器解决缓存穿透问题
    //如果数据变更较少就用string类型，数据变更多再考虑hash那些结构
    //没有防缓存穿透
    public Result queryShopByIdPassThrough(Long id) {
        String key = CACHE_SHOP_PREFIX + id;

        // 1. 从Redis查询JSON字符串
        String json = stringRedisTemplate.opsForValue().get(key);

        // 2. 缓存命中，isNotBlank会判断null和空字符串的情况
        if (StrUtil.isNotBlank(json)) {
            Shop shop = JSONUtil.toBean(json, Shop.class);
            return Result.ok(shop);
        }

        // 3. 缓存空值（防穿透）
        if (json != null) {
            return Result.fail("店铺不存在");
        }

        // 4. 查询数据库
        Shop shop = getById(id);

        // 5. 数据库不存在->缓存空值
        //感觉用枚举反而不清晰
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key, "", CacheTTL.NULL.getValue(),CacheTTL.NULL.getUnit());
            return Result.fail("店铺不存在");
        }

        // 6. 数据库存在->写缓存
        String shopJson = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, shopJson, CacheTTL.SHOP.getValue(), CacheTTL.SHOP.getUnit());

        return Result.ok(shop);
    }

    //TODO 这个实现不能在分布式系统中生效，要再分布式系统中实现要用分布式事务，比如tcc
    //cache Aside更新策略，先更新数据库，再更新缓存
    @Transactional
    @Override
    public Result updateShop(Shop shop) {
        String key = CACHE_SHOP_PREFIX + shop.getId();
        Long id = shop.getId();

        //防小人
        if (id == null) {
            return Result.fail("id不能为空");
        }

        //先读数据库
        boolean success = updateById(shop);
        if (!success) {
            return Result.fail("更新失败，记录不存在");
        }

        //再删缓存
        stringRedisTemplate.delete(key);

        return Result.ok();
    }
}
